/*
 * Wildcard.java
 *
 * Created on May 24, 2007, 2:51 AM
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.wildcard;

import java.util.Arrays;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.parsing.Tokenizer;
import tal.drivechain.parsing.ParsingAid;
import tal.drivechain.variable.types.DataType;

/**
 * A compiled representation of a Wildcard expression. 
 * The Wildcard class can determine if a given value is in the range of the
 * Wildcard expression.
 * <p>
 * A Wildcard object can be created either by using Wildcard.parseDefinition() or
 * by passing parameters to the Wildcard constructor.
 * <p>
 * To create a Wildcard from parseDefinition use:
 * Wildcard w = Wildcard.parseDefinition("[0..20]");
 * This will create a Wildcard object that represents any number between 0 and 20.
 * It uses the range (..) operator (OPER_RANGE). Every Wildcard object can have one operator and a 
 * number of child Wildcards or one value (Represented by an object).
 * For example the wildcard [0..20] consists of two child Wildcards (containing the 
 * values 0 and 20 with the number operator (OPER_ELEMENT) and the range operator. 
 * Therefore to create the same object using it's constructor you would use:
 * Wildcard a = new Wildcard(null, "0", Wildcard.OPER_ELEMENT);
 * Wildcard b = new Wildcard(null, "20", Wildcard.OPER_ELEMENT);
 * Wildcard w = new Wildcard(new Wildcard[] { a, b }, null, Wildcard.OPER_RANGE);
 * 
 * To check if a value matches the Wildcard use:
 * if (w.matches(x, localVars)) { }
 * 
 * As you can see wildcards are nested in each other by nature thus allowing complex value 
 * expressions such as: [*;[!0,3.5,25]] (any value except 0, 3.5 and 25).
 * Instead of number values wildcards can also hold any other mathematical expression that 
 * can be evaluated by the Expression class: 
 * [{pi*3},{pi*10}] for example would match only 3pi or 10pi.
 * <p>
 * Wildcard operators:
 * OPER_ANY (*)     - Always returns true (matches any value)
 * OPER_AND (;)     - Returns true if a value matches every element Wildcard. 
 *                    [0;*;[!5,6]] would match only zero since it has to match every element Wildcard.
 * OPER_OR (,)      - Returns true if a value matches any of the element Wildcards.
 *                    [0,*,[!5,6]] would match every value since it contains the any operator.
 * OPER_RANGE (..)  - Returns true if the value is in the range between number values. accepts either x..y, ..x, x..
 *                    [0..20] would match any number between 0 and 20 including 0 and 20. [..20] would match any number that is lower or equals 20.
 * OPER_NOT (!)     - Returns true when it's element wildcard returns false.
 *                    [!5,6,7] would match any number except 5, 6 and 7. 
 * OPER_ELEMENT         - Returns true if the value matches it's element Expression value
 *                    [5.67] matches only 5.67 etc.
 * 
 * OPER_ANY (*) and OPER_ELEMENT and OPER_NOT are the only Wildcards that may not be surrounded by brackets.
 * For example [*;!6] is the same as [[*];[![6]]].
 * 
 * 
 * @author Tal Eisenberg
 */
public class Wildcard {
    public static enum Operator { AND, OR, RANGE, NOT, ANY, ELEMENT }
   
    private Wildcard[] childrenWildcards = null;
    private Operator operator = null;
    private WildcardElement element = null;
    
    /**
     * Returns the Wildcards operator code.
     */
    public Operator getOperator() { return operator; }

    /**
     * Class constructor. Creates a Wildcard object according to the parameters.
     * 
     * 
     * 
     * @param childrenWildcards  An array of element Wildcards that are connected with the operator.
     * @param element            A WildcardElement object that is used as an element when operator equals OPER_ELEMENT
     * @param operator           An integer specifying the operator of the Wildcard according to the OPER fields.
     * @throws IllegalArgumentException     if elementExpression can't be parsed or operator is not a valid operator code.
     */
    public Wildcard(Wildcard[] childrenWildcards, WildcardElement element, Operator operator) throws IllegalArgumentException {
        this.childrenWildcards = childrenWildcards;
        this.element = element;
        this.operator = operator;
        checkOperators(operator);        
    }
    
    /**
     *  Returns a Wildcard object according to the definition String wc.
     *
     *  @param wc   A valid Wildcard definition String.
     *  @return     A new Wildcard object created out of the definition String.
     *  @throws IllegalArgumentException    If the definition format is invalid.
     */
    public static Wildcard parseDefinition(String wc, WildcardElement elementClass) throws IllegalArgumentException {
        // adjust to strict wildcard format
        if (wc==null || wc.trim().length()==0) throw new IllegalArgumentException("Null wildcard string.");
        if (wc.trim().equals("*")) wc = "[*]";
        if (elementClass.matchesElementFormat(wc)) wc = "[" + wc + "]";
        if (wc.charAt(0)!='[' || wc.charAt(wc.length()-1)!=']') throw new IllegalArgumentException("Wildcard must be surrounded by brackets (" + wc + ").");
        if (Tokenizer.balanced(wc,0,'[',']')==wc.length()-1)
            wc = wc.substring(1, wc.length()-1);
        if (wc.equals("*")) return new Wildcard(null, null, Operator.ANY);
        if (wc.equals("")) throw new IllegalArgumentException("Null wildcard string.");
        
        Tokenizer wildcardTokenizer = new Tokenizer(wc, ';');
        Tokenizer selectionTokenizer = new Tokenizer(wc, ',');
        
        // check for AND tokens (;)
        if (wildcardTokenizer.getTokens().length>1) { // has more than 1 tokens [x];[y];...
            Wildcard[] wcs = new Wildcard[wildcardTokenizer.getTokens().length];
            for (int i=0; i<wildcardTokenizer.getTokens().length; i++) {
                String wildtoken = wildcardTokenizer.getTokens()[i];
                if (wildtoken.charAt(0)!='[' && wildtoken.charAt(wildtoken.length()-1)!=']')
                    wildtoken = "[" + wildtoken + "]";
                wcs[i] = Wildcard.parseDefinition(wildtoken, elementClass);
            }
            return new Wildcard(wcs, null, Operator.AND);
        } else if (selectionTokenizer.getTokens().length>1) {
            // check for OR tokens (,)
            String stok;
            int i=0;
            Wildcard[] wcs = new Wildcard[selectionTokenizer.getTokens().length];
            while (selectionTokenizer.hasMoreTokens()) {
                stok = selectionTokenizer.nextToken().trim();
                if (stok.charAt(0)!='[' && stok.charAt(stok.length()-1)!=']')
                    stok = "[" + stok + "]";
                wcs[i] = Wildcard.parseDefinition(stok, elementClass);
                i++;
            }
            return new Wildcard(wcs, null, Operator.OR);
        } else if (wc.charAt(0)=='!') {
            Wildcard[] wcs = new Wildcard[1];
            wcs[0] = Wildcard.parseDefinition("[" + wc.substring(1) + "]", elementClass);
            return new Wildcard(wcs, null, Operator.NOT);
        } else if (wc.matches(".*\\.\\..*")) {
            String nums[] = wc.split("\\.\\.");
            if (nums.length<=2) {
                Wildcard[] wcs = new Wildcard[2];
                if (nums.length>0 && nums[0].trim().length()>0)
                    wcs[0] = new Wildcard(null, elementClass.makeWildcardElement(nums[0]), Operator.ELEMENT);
                else wcs[0] = null;
                if (nums.length>1 && nums[1].trim().length()>0)
                    wcs[1] = new Wildcard(null, elementClass.makeWildcardElement(nums[1]), Operator.ELEMENT);
                else wcs[1] = null;
                if (wcs[0]==null && wcs[1]==null)
                    throw new IllegalArgumentException("A range wildcard must have at least one element (" + wc + ").");
                return new Wildcard(wcs, null, Operator.RANGE);
            } else throw new IllegalArgumentException("A range wildcard can't have more than two elements (" + wc + ").");
        } else return new Wildcard(null, elementClass.makeWildcardElement(wc), Operator.ELEMENT);
    }
    
    /**
     *  Checks if a value is in the Wildcards group of values.
     *
     *  @param value        The value that is checked against the Wildcard
     *  @param localVars    A VarTable containing local variables that might be needed
     *                      to evaluate the value Expression of the Wildcard. Can also be null.
     *  @return             True if value matches the Wildcard.
     *  @throws IllegalArgumentException        if the value Expression throws an IllegalArgumentException.
     */
    public boolean matches(Object value, ChainNode context) throws IllegalArgumentException {
        switch (operator) {
            case ANY:
                return true;
            case OR:
                if (childrenWildcards==null || childrenWildcards.length==0) return false;
                for (int i=0; i<childrenWildcards.length; i++)
                    if (childrenWildcards[i].matches(value, context)) return true;
                return false;                
            case AND:
                for (int i=0; i<childrenWildcards.length; i++)
                    if (!childrenWildcards[i].matches(value, context)) return false;
                return true;
            case NOT:
                return !(childrenWildcards[0].matches(value, context));
            case RANGE: {
                if (childrenWildcards==null || childrenWildcards.length!=2) return false;
                
                double dValue = (Double)DataType.FLOAT.cast(value);                
                if (childrenWildcards[0]==null) { //less than [..x]
                    double num = (Double)childrenWildcards[1].getWildcardElement().getValue(context);
                    return dValue<=num;
                } else if (childrenWildcards[1]==null) { // greater than [x..]
                    double num = (Double)childrenWildcards[0].getWildcardElement().getValue(context);
                    return dValue>=num;
                } else { //normal range
                    double firstnum = (Double)childrenWildcards[0].getWildcardElement().getValue(context);
                    double secondnum = (Double)childrenWildcards[1].getWildcardElement().getValue(context);
                    if (firstnum<secondnum)
                        return (dValue >= firstnum && dValue <= secondnum);
                    else return (dValue >= secondnum && dValue <= firstnum);        
                }
            } case ELEMENT:
                if (element==null) return false;
                else return element.matches(value, context);
            default: return false;
        }
    }
    
    /**
     *  Checks if a String can be a Wildcard definition String.
     *
     *  @param wc   This String is analyzed to see if it can be a Wildcard definition
     *  @return     True if wc looks like a Wildcard definition.
     */
    public static boolean isWildcard(String wc) {
        wc = wc.trim();
        if (wc.equals("*")) return true;
        else if (wc.charAt(0)=='[' && wc.charAt(wc.length()-1)==']') return true;
        else if (ParsingAid.isNumber(wc)) return true;
        else return false;
    }
    
    public WildcardElement getWildcardElement() { return element; }
    
    /**
     *  Constructs recursively a String that represents the Wildcard object. 
     *  The returned String is not a copy of the original definition String 
     *  and any Expression in the original Wildcard definition will be evaluated.
     *
     *  @param localVars        A VarTable that's used for evaluating Expression objects that might be contained in the Wildcard tree.
     *  @throws IllegalArgumentException    In case getNumber() throws an exception while retrieving it's value.
     */
    public String getWildcardString(ChainNode context) throws IllegalArgumentException {
        switch (operator) {
            case ELEMENT:
                return ""+getWildcardElement().toString(context);
            case RANGE:
                String out_range = "[";
                if (childrenWildcards[0]!=null)
                    out_range += childrenWildcards[0].getWildcardString(context);
                out_range += "..";
                if (childrenWildcards[1]!=null)
                    out_range += childrenWildcards[1].getWildcardString(context);
                return out_range + "]";
            case NOT:
                return "[!" + childrenWildcards[0].getWildcardString(context) + "]";
            case ANY:
                return "[*]";
            case OR:
                String out_or = "[";
                for (int i=0; i<(childrenWildcards.length-1); i++) 
                    out_or += childrenWildcards[i].getWildcardString(context) + ",";
                out_or += childrenWildcards[childrenWildcards.length-1].getWildcardString(context) + "]";
                return out_or;
            case AND:
                String out_and = "[";
                for (int i=0; i<(childrenWildcards.length-1); i++) 
                    out_and += childrenWildcards[i].getWildcardString(context) + ";";
                out_and += childrenWildcards[childrenWildcards.length-1].getWildcardString(context) + "]";
                return out_and;
            default:
                return "Unknown operator code: " + operator.name();                
        }
    }

    public void checkOperators(Operator op) {
        if (element!=null) {
            if (!element.validOperator(op))
                throw new IllegalArgumentException("Invalid wildcard operator for element type " + element.getClass().getSimpleName() + ": " + op.name());
        } else { // look in children elements
            if (childrenWildcards!=null) {
                for (Wildcard c : this.childrenWildcards) {
                    if (c!=null) c.checkOperators(op);
                }
            }
        }
    }

    @Override
    public String toString() {
        return getWildcardString(null);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Wildcard) {
            if (super.equals(obj)) return true;
            else {
                Wildcard that = (Wildcard)obj;
                if (this.element!=null && !this.element.equals(that.element)) return false;
                if (this.operator!=null && !this.operator.equals(that.operator)) return false;
                if (this.childrenWildcards!=null) {
                    for (Wildcard c : this.childrenWildcards) {
                        // try to find an equal child somewhere in that's array
                        boolean found = false;
                        for (Wildcard t : that.childrenWildcards)
                            if (c.equals(t))
                                found = true;

                        if (!found) return false;
                    }
                } else return (that.childrenWildcards==null);
                return true; // passed all tests.
            }
        } else return false;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 71 * hash + Arrays.deepHashCode(this.childrenWildcards);
        hash = 71 * hash + (this.operator != null ? this.operator.hashCode() : 0);
        hash = 71 * hash + (this.element != null ? this.element.hashCode() : 0);
        return hash;
    }

}

